引言
在构建个人博客或技术文档时,无后端方案因其简单、高效和低成本而备受青睐。本文将介绍如何使用 Next.js、ContentLayer 和 MDX 构建一个无后端博客系统(本博客当前采用的方案),并探讨如何后续迁移到 MDX-Bundler 和 GitHub API,利用 GitHub Issues 作为文章存储源。
技术栈简介
- Next.js:React 框架,支持 SSR、SSG 和 API 路由,适合构建高性能博客。
- ContentLayer:将 Markdown 或 MDX 文件转换为类型安全的 JSON 数据,方便在 Next.js 中使用。
- MDX:支持在 Markdown 中嵌入 React 组件,增强文章的表现力。
- MDX-Bundler:将 MDX 文件编译为 React 组件,支持动态加载和自定义组件。
- GitHub API:通过 GitHub Issues 存储文章内容,实现无后端数据管理。
使用 Next.js + ContentLayer + MDX 构建博客系统
初始化项目
SH- 1npx create-next-app@latest my-blog
- 2cd my-blog
- 3npm install contentlayer @mdx-js/loader
配置 ContentLayer
在项目根目录创建 contentlayer.config.ts 文件:
TScontentlayer.config.ts- 1import { defineDocumentType, makeSource } from "contentlayer/source-files";
- 2
- 3export const Post = defineDocumentType(() => ({
- 4 name: "Post",
- 5 filePathPattern: `**/*.mdx`,
- 6 fields: {
- 7 title: { type: "string", required: true },
- 8 date: { type: "date", required: true },
- 9 },
- 10 computedFields: {
- 11 slug: {
- 12 type: "string",
- 13 resolve: (post) => post._raw.flattenedPath,
- 14 },
- 15 },
创建文章
在 data/posts 目录下创建 Markdown 或 MDX 文件:
MDXdata/posts/hello.mdx
- 1---
- 2title: "Hello, Next.js!"
- 3date: 2023-10-01
- 4---
- 5
- 6This is a blog post written in **MDX**.
实现文章列表页
在 src/app/page.tsx 中实现文章列表:
TSXsrc/app/page.tsx- 1import { allPosts } from "contentlayer/generated";
- 2import Link from "next/link";
- 3
- 4export default function Home() {
- 5 return (
- 6 <div>
- 7 <h1>Blog Posts</h1>
- 8 <ul>
- 9 {allPosts.map((post) => (
- 10 <li key={post.slug}>
- 11 <Link href={`/posts/${post.slug}`}>{post.title}</Link>
- 12 </li>
- 13 ))}
- 14 </ul>
- 15 </div>
创建 mdx 渲染组件
在 src/components/mdx.tsx 中实现 mdx 渲染组件:
TSXsrc/components/mdx.tsx- 1import { useMDXComponent } from "next-contentlayer/hooks";
- 2
- 3export interface MdxProps {
- 4 code: string;
- 5}
- 6
- 7export default function Mdx({ code }: MdxProps) {
- 8 const Component = useMDXComponent(code);
- 9 return <Component />;
- 10}
实现文章详情页
在 src/app/posts/[slug].tsx 中实现文章详情页:
TSXpages/posts/[...slug].tsx- 1import Mdx from "@/src/components/mdx";
- 2import { allPosts } from "contentlayer/generated";
- 3
- 4interface IProps {
- 5 params: { slug: string[] };
- 6}
- 7
- 8export default function Post({ params }: IProps) {
- 9 const slug = decodeURIComponent(params.slug.join("/"));
- 10 const postIdx = allPosts.findIndex((p) => p.slug === slug);
- 11 const post = allPosts[postIdx];
- 12 if (!post) return notFound();
- 13
- 14 return (
- 15 <div>
迁移到 MDX-Bundler + GitHub API
安装 MDX-Bundler
SH
- 1npm install mdx-bundler
根据 repo 和 name 获取 issues
TSsrc/utils/github.ts- 1const REPO = "your repo";
- 2const NAME = "your name";
- 3
- 4export const fetchGithubIssueList = (type: "page" | "post", current: number) =>
- 5 fetch(
- 6 `https://api.github.com/search/issues?q=repo:${REPO}+state:open+author:${NAME}+${encodeURIComponent(
- 7 `[${type}]`
- 8 )}+in:title&per_page=10&sort=updated&page=${current}`
- 9 ).then((res) => res.json());
- 10
- 11export const fetchGithubIssueDetail = (id: number) =>
- 12 fetch(`https://api.github.com/repos/${REPO}/issues/${id}`).then((res) =>
- 13 res.json()
- 14 );
使用 MDX-Bundler 编译文章
TSsrc/utils/parse-mdx.ts- 1import { bundleMDX } from "mdx-bundler";
- 2
- 3export const parseMDX = (content: string) =>
- 4 bundleMDX({
- 5 source: content,
- 6 esbuildOptions: (opts) => {
- 7 opts.target = "es2020";
- 8 return opts;
- 9 },
- 10 });
改写文章列表页面
TSXsrc/app/page.tsx- 1import { fetchGithubIssueList } from "@/src/utils/github";
- 2import Link from "next/link";
- 3
- 4export default async function Home() {
- 5 const allPosts = await fetchGithubIssueList("post", 1);
- 6 return (
- 7 <div>
- 8 <h1>Blog Posts</h1>
- 9 <ul>
- 10 {allPosts.items.map((post) => (
- 11 <li key={post.id}>
- 12 <Link href={`/posts/${post.number}`}>{post.title}</Link>
- 13 </li>
- 14 ))}
- 15 </ul>
改写文章详情页面
TSXpages/posts/[slug].tsx- 1import { parseMDX } from "@/src/utils/parse-mdx";
- 2import { fetchGithubIssueDetail } from "@/src/utils/github";
- 3
- 4interface IProps {
- 5 params: { slug: string };
- 6}
- 7
- 8export default async function Post({ params }: IProps) {
- 9 const post = await fetchGithubIssueDetail(params.slug);
- 10 if (post.status === "404") return notFound();
- 11 const { code } = await parseMDX(post.body);
- 12 return (
- 13 <div>
- 14 <h1>{post.title}</h1>
- 15 <div>
总结
通过 Next.js + ContentLayer + MDX,我们可以快速构建一个无后端博客系统。而迁移到 MDX-Bundler + GitHub API 后,我们可以利用 GitHub Issues 作为文章存储源,实现更灵活的内容管理。这种方案不仅简单高效,还能充分利用现有的工具和平台,适合个人开发者和小型团队。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 liangerwen's☻Blog !

